<?php

abstract class Workset_Model_Object_Abstract implements Workset_Model_Object_Interface,
														Workset_Model_Subject_Interface {

	/**
	 * Ограничение по умолчанию на выборку списка объектов
	 */
    const ROWS_LIMIT = 500;

	/**
	 * Поле для сортировки
	 */
    const SORT_FIELD = 'id';

	/**
	 * Направление сортировки
	 */
    const SORT_ORDER = 'ASC';

	/**
	 * Пути для подгрузки классов
	 *
	 * @var array
	 */
	protected static $_pluginLoaders = array(
		'filter' => array('Workset_Filter' => 'Workset/Filter')
	);

	protected static $_registry = null;

    protected static $_dateFormat = "dd.MM.YYYY";

	protected static $_defaultStorages = array();

	protected static $_defaultObservers = null;

	protected $_objectType = null;

	protected $_throwNullPointerException = true;

    /**
     * Исходные поля объекта (возвращаемые из хранилища Storage)
     */
	protected $_data = null;

	/**
	 * Если поля объекта модифицированы, то тут сохраняются старые значения
	 */
	protected $_oldData = null;

    /**
     * Дополнительные поля объекта уровня приложения
     */
    protected $_appData = array(
        'location'  => array(),
        'label'     => array(),
    );

	/**
	 * Был ли объект сериализован
	 */
	protected $_serialized = false;

    /**
     * @var string
     *
     * Объект для доступа к даннам
     */
    protected $_storage = null;

    /**
     * @var bool
     *
     * Имеет ли объект связи с другими объектами
     */
    protected $_hasRelations = false;

    /**
     * Настройки объекта
     *
     * @var array
     */
    protected $_options = array();

    /**
     * Флаг необходимости обновления полей объекта в базе
     * @var bool
     *
     */
	protected $_update = false;

	/**
	 * Список зарегистрированных Observer-ов
	 * 
	 * @var SplObjectStorage
	 */
    protected $_observers = null;

	/**
	 * Список помошников
	 * 
	 * @var array
	 */
	protected $_helpers = array();

	/**
	 * @static
	 * @param  ArrayObject $registry
	 * @param  $key
	 * @return void
	 *
	 * Устанавливает контейнер для кеширования объектов
	 */
	static function setRegistry(ArrayObject $registry, $key = null) {
        if (null !== $key) {
            if (!isset($registry[$key])) {
                $registry[$key] = array();
            }
            self::$_registry = & $registry[$key];
        } else {
            self::$_registry = $registry;
        }
    }

	/**
	 * @static
	 * @return ArrayObject
	 *
	 * Возвращает контейнер для кеширования объектов
	 */
    static function getRegistry() {
        if (null === self::$_registry) {
           self::$_registry = new ArrayObject();
        }
        return self::$_registry;
    }

	/**
	 * @static
	 * @param  $key
	 * @return void
	 *
	 * Очищает контейнер для кеширования объектов
	 */
	static function clearRegistry($key = null) {
		$registry = self::getRegistry();
		if (null !== $key) {
			if (isset($registry[$key])) {
				unset($registry[$key]);
			}
		} else {
			$registry = null;
		}
	}

	/**
	 * @static
	 * @param  $key
	 * @param  $object
	 * @return void
	 *
	 * Добавляет в кеш новый объект
	 */
	static function addToRegistry($key, $object, $overwrite = false) {
		$registry = self::getRegistry();
		if (!isset($registry[$key]) || true == $overwrite) {
			$registry[$key] = $object;
		}
	}

	/**
	 * Устанавливает объект Storage по умолчанию
	 *
	 * @static
	 * @throws Workset_Model_Exception
	 * @param  $type
	 * @param  $storage
	 * @param  $options
	 * @return void
	 */
	static function setDefaultStorage($type, $storage, $options = null) {
		if (is_string($storage)) {
			$storage = Workset_Model_Object_Storage_Abstract::getStatic($storage);
		} elseif (!$storage instanceof Workset_Model_Object_Storage_Abstract) {
			throw new Workset_Model_Exception('Invalid storage type');
		}

		if (null !== $options) {
			$storage->setOptions($options);
		}

		self::$_defaultStorages[$type] = $storage;
	}

	/**
	 * Возвращает объект Storage, если он установлен
	 *
	 * @static
	 * @throws Workset_Model_Exception
	 * @param  $type
	 * @return
	 */
	static function getDefaultStorage($type) {
		if (!isset(self::$_defaultStorages[$type])) {
			throw new Workset_Model_Exception('Undefined storage by type '.$type);
		}
		return self::$_defaultStorages[$type];
	}

	/**
	 * @param Workset_Model_Object_Storage_Abstract $storage
	 * @param  $options
	 * @return Workset_Model_Object_Abstract
	 *
	 * Устанавливает объект Storage (для работы с постоянным хранилещем)
	 */
	function setStorage(Workset_Model_Object_Storage_Abstract $storage, $options = null) {
		if ($this->_throwNullPointerException && !$storage->throwNullPointerException()) {
			$options = (array)$options;
			$options['throwNullPointerException'] = true;
		}

		if ($options) {
			$storage->setOptions($options);
		}

		$this->_storage = $storage;
        return $this;
	}

    /**
     * @throws Exception
     * @param  $storage
     * @param array $options
     * @return Workset_Model_Object_Storage_Table_Abstract
     */
    function getStorage($throwExceptionIfNull = true) {
        $this->_initStorage();
        if (null === $this->_storage && $throwExceptionIfNull) {
			throw new Workset_Model_Exception("Undefined default storage with object ".get_class($this));
		}
		return $this->_storage;
    }

	/**
	 * Инициализация объекта Storage
	 *
	 * @throws Workset_Model_Exception
	 * @param  $type
	 * @return string
	 */
	protected function _initStorage($type = null) {
		if (null !== $type) {
			$storage = self::getDefaultStorage($type);
            if (null === $this->_storage && $type === $this->_objectType) {
                $this->_storage = $storage;
            }
		} elseif (null === $this->_storage) {
			if (is_string($this->_objectType)) {
				$storage = $this->_storage = self::getDefaultStorage($this->_objectType);
			} else {
				throw new Workset_Model_Exception("Undefined default storage with object ".get_class($this));
			}
		} else {
			$storage = $this->_storage;
		}

		return $storage;
	}

	/**
	 * Устанавливает опции для объекта
	 *
	 * @param  $options
	 * @return void
	 */
	function setOptions(array $options) {
		if (!$this->_options) {
			$this->_options = $options;
		} else {
			$this->_options = array_merge($this->_options, $options);
		}

		foreach ($this->_options as $key => $option) {
			$key = strtolower($key);

			switch ($key) {
				case 'storage':
					if (null !== $option) {
						$this->setStorage($option);
					}
					unset($this->_options[$key]);
				break;
			}
		}
	}

	/**
	 * Multiton implementation
	 *
	 * Возвращает объект из постоянного хранилища (например БД).
	 * Возвращаемый объект кешируется
	 *
	 * @static
	 * @param  Workset_Model_Object_Storage_Interface $storage
	 * @param  $identity
	 * @return Workset_Model_Object_Abstract
	 */
	static function getFromStorage($storage, $identity) {
        if (is_string($storage)) {
            $storage = Workset_Model_Object_Storage_Abstract::getStatic($storage);
        }

        if (!$storage instanceof Workset_Model_Object_Storage_Abstract) {
            throw new Workset_Model_Exception("Undefined storage class");
        }

		$key = $storage->getCacheId($identity);

		$registry = self::getRegistry();
		if (isset($registry[$key])) {
			return $registry[$key];
		}

		$object = $storage->getOne($identity);
        if (null !== $object) {
            self::addToRegistry($key, $object);
            return $object;
        } else {
			throw new Workset_Model_Exception("Object by identity ".implode(',', (array)$identity)." not found in storage ".get_class($storage));
		}
	}

	/**
	 * Возвращает список объектов из постоянного хранилища (например БД).
	 *
	 * @static
	 * @throws Exception
	 * @param  $storage
	 * @param  $where
	 * @param  $order
	 * @param  $limit
	 * @param  $offset
	 * @return Workset_Model_Object_Iterator_Abstract
	 */
	static function getAllFromStorage($storage, $where = null, $order = null, $limit = null, $offset = null) {
		if (is_string($storage)) {
            $storage = Workset_Model_Object_Storage_Abstract::getStatic($storage);
        }

        if (!$storage instanceof Workset_Model_Object_Storage_Abstract) {
            throw new Workset_Model_Exception("Undefined storage class $storage");
        }

		return $storage->getAll($where, $order, $limit, $offset);
	}

    /**
     * Приводит дату к внутреннему формату (для хранения в БД)
	 *
	 * @static
     * @param  $date
     * @return int
     */
    static function inputDateFormat($date, $locale = null) {
        if (!isTimestamp($date)) {
            $filter = self::getPlugin('filter', 'DateToTimestamp');
            if (null !== $locale) {
                $filter->setOptions(array(
                    'locale' => $locale
                ));
            }
            return $filter->filter($date);
        }
        return $date;
    }

    /**
     * Преобразует дату к формату сайта
	 *
	 * @static
     * @param  $date
     * @return string
     */
    static function outputDateFormat($date, $locale = null, $format = null) {
        if (isTimestamp($date)) {
            $filter = self::getPlugin('filter', 'TimestampToDate');

            $filter->setOptions(array(
                'locale' => $locale,
                'format' => $format
            ));

            return $filter->filter($date);
        }
        return $date;
    }

    /**
     * Преобразует значение поля BIT в integer
     *
     * @static
     * @param  $bin
     * @return int
     *
     */
	static function bit2Int($bin) {
		return (int)bin2hex($bin);
	}

    /**
     * Проверяет наличие битового флага
     *
     * @static
     * @param  $flag
     * @param int $flags
     * @return int
     */
    static function isFlag($flag, $flags = 0) {
        return $flag & (int)$flags;
    }

    /**
     * Возвращает сумму битовых флагов
     *
     * @static
     * @param  $flags
     * @return int
     */
    static function flags2Int($flags) {
        if (is_array($flags)) {
            $flags = array_unique($flags);
            if (count($flags) > 31) {
                throw new Workset_Model_Exception("Invalid flag value");
            }
            $flags = (int)array_sum($flags);
        } else {
            $flags = (int)$flags;
        }

        return $flags;
    }

	/**
	 * Регистрирует Observer по умолчанию для всех объектов
	 *
	 * @static
	 * @param Workset_Model_Observer_Interface $observer
	 * @return void
	 */
	static function attachDefaultObserver(Workset_Model_Observer_Interface $observer) {
		if (null === self::$_defaultObservers) {
            self::$_defaultObservers = new SplObjectStorage();
        }
        self::$_defaultObservers->attach($observer);
	}

	/**
	 * Удаляет Observer по умолчанию для всех объектов
	 *
	 * @static
	 * @param Workset_Model_Observer_Interface $observer
	 * @return void
	 */
	static function detachDefaultObserver(Workset_Model_Observer_Interface $observer) {
		if (null !== self::$_defaultObservers) {
            self::$_defaultObservers->detach($observer);
        }
	}

	function __construct($data, $options = null) {
		if (is_array($data) || $data instanceof Workset_Model_Row_Interface) {
			$this->_data = $data;
		} else {
			throw new Workset_Model_Exception('Invalid data type in object '.get_class($this));
		}

		if (null !== $options) {
			$this->setOptions($options);
		}

		$this->init();
	}

    /**
     * Дополнительная инициализация объекта 
     *
     * @return void
     */
	function init() { }

	/**
     * @param  $type
     * @param  $url
     * @return void
     *
     * Устанавливает URL объекта
     */
    function setUrl($type, $url) {
        return $this->setParam('location', array($type => $url));
    }

    /**
     * @param  $type
     * @return array|string
     *
     * Возвращает URL объекта
     */
    function getUrl($type = null) {
        return $this->getParam('location', $type);
    }

    /**
     * @param  $type
     * @param  $value
     * @return Catalogue_Model_Object_Abstract
     *
     * Устанавливает название метки
     */
    function setLabel($type, $value) {
        return $this->setParam('label', array($type => (string)$value));
    }

    /**
     * @param  $type
     * @return string
     *
     * Возвращает название метки
     */
    function getLabel($type = null) {
        return $this->getParam('label', $type);
    }

    /**
     * @return array
     *
     * Возвращает массив полей объекта
     */
    function toArray() {
        if ($this->_data instanceof Workset_Model_Row_Interface) {
            return array_merge($this->_data->toArray(), $this->_appData);
        } else {
            return array_merge($this->_data, $this->_appData);
        }
    }

    /**
     * Возвращает поля объекта
     *
     * @return array|Workset_Model_Row_Interface
     */
	function getData() {
		return $this->_data;
	}

    /**
     * Устанавливает новые значения полей объекта
     *
     * @param  array|Workset_Model_Row_Interface $data
     * @return Workset_Model_Object_Abstract
     */
	function setData($data) {

		if (!is_array($data)) {
            if ($data instanceof Workset_Model_Row_Interface) {
                $data = $data->toArray();

            } elseif ($data instanceof IteratorAggregate) {
				$data = $data->getIterator();

			} elseif (!$data instanceof Iterator) {
                throw new Users_Model_Exception('Invalid data format');
            }
        }

		$this->_saveOldData();

		foreach ($data as $key => $value) {
			if (isset($this->_data[$key])) {
				$this->_data[$key] = $value;
			}
		}

        $this->_update = true;

		return $this;
	}

	protected function _saveOldData() {
		if (!is_array($this->_data)) {
			$this->_oldData = $this->_data->toArray();
		} else {
			$this->_oldData = $this->_data;
		}
	}

	function getOldData() {
		return $this->_oldData;
	}

    /**
     * Устанавливает дополнительное поле объекта
     *
     * @param  $name
     * @param  $value
     * @return Workset_Model_Object_Abstract
     */
    function setParam($name, $value) {
        if (is_array($value)) {
            $this->_appData[$name][key($value)] = current($value);
        } else {
			$this->_appData[$name] = $value;
        }
        return $this;
    }

	/**
	 * Устанавливает массив дополнительных полей объекта
	 *
	 * @param  $params
	 * @return Workset_Model_Object_Abstract
	 */
	function setParams(array $params) {
		foreach ($params as $key => $value) {
			$this->setParam($key, $value);
		}
		return $this;
	}

    /**
     * @param  $name
     * @param  $key
     * @return array
     *
     * Возвращает значение дополнительного поля объекта
     */
    function getParam($name, $key = null) {
        if (isset($this->_appData[$name])) {
            if (null !== $key && is_array($this->_appData[$name])) {
                return $this->_appData[$name][$key];
            } else {
                return $this->_appData[$name];
            }
        }
        return null;
    }

	/**
	 * Возвращает список дополнительных полей объекта
	 *
	 * @return array
	 */
	function getParams() {
		return $this->_appData;
	}

	/**
	 * Очищает дополнительные поля объекта,
	 * если передан $keys, то очищает только перечисленные в $keys поля
	 *
	 * @param  $keys
	 * @return Catalogue_Model_Object_Abstract
	 */
    function clearParams($keys = null) {
        if (null !== $keys) {
            if (is_string($keys)) {
                unset($this->_appData[$keys]);
            } else {
                $keys = (array)$keys;
                $this->_appData = array_diff_key($this->_appData, array_combine($keys, array_fill(0, count($keys), null)));
            }
        } else {
            $this->_appData = array();
        }
		return $this;
    }

	/**
	 * @return mixed
	 *
	 * Возвращает ID объекта
	 */
	function getId() {
		return isset($this->_data['id']) ? (int)$this->_data['id'] : null;
	}

    /**
     * Был ли объект сохранен в базе
     *
     * @return bool
     */
    function isStored() {
        return $this->getId() > 0;
    }

	/**
	 * Сохраняет объект в постоянном хранилище,
	 * возвращает ID сохраненного объекта
	 *
	 * @return int
	 */
	function save() {

        //call observers
        $this->notifyBeforeSave();

		$storage = $this->getStorage();
		$id = $storage->save($this);

		if (!empty($id)) {
			$key = $storage->getCacheId($id);
			self::clearRegistry($key);

            $this->_setId($id);
		}

        $this->setIsModified(false);

		//call observers
		$this->notifyAfterSave();

		//сбрасываем старые значения
		$this->_oldData = null;        

		return $this->getId();
	}

	/**
	 * Удаляет объект из постоянного хранилища,
	 * возвращает ID удаленного объекта, null если не удалось удалить объект
	 *
	 * @return int
	 */
	function delete() {

        //ставим принудительно, чтобы можно было отменить удаление в обсерверах
        $this->setIsModified(true);

        //call observers
        $this->notifyBeforeDelete();

        //если удаление не требуется
        if (!$this->isModified()) {
            return null;
        }

        $id = $this->getId();

		$storage = $this->getStorage();

        $this->_saveOldData();

		if ($storage->delete($this)) {

			//call observers
        	$this->notifyAfterDelete();

			$key = $storage->getCacheId($id);
			self::clearRegistry($key);
		}

		return $id;
	}

    /**
     * Обновляет объект в хранилище только если флаг $this->_update выставлен в true
     *
     * @return int
     */
    function updateData() {
        if ($this->isModified()) {
            return $this->save();
        }
    }

    function setIsModified($flag = true) {
        $this->_update = (bool)$flag;
    }

    function isModified() {
        return $this->_update && $this->getId() > 0;
    }

    function attach(Workset_Model_Observer_Interface $observer) {
        if (null === $this->_observers) {
            $this->_observers = new SplObjectStorage();
        }

        $this->_observers->attach($observer);

        return $this;
    }

    function detach(Workset_Model_Observer_Interface $observer) {
        if (null !== $this->_observers) {
            $this->_observers->detach($observer);
        }

        return $this;
    }

    protected function _notify($method, $data = null) {

        //чтобы избежать проблемы с rewind в цикле, если обработчик
        //порождает вызовы других обработчиков у текущего объекта
        $storage = new SplObjectStorage();

		if (null !== self::$_defaultObservers) {
            $storage->addAll(self::$_defaultObservers);

			/*foreach (self::$_defaultObservers as $object) {
                $object->{$method}($this, $data);
            }*/
		}

        if (null !== $this->_observers) {
            $storage->addAll($this->_observers);

            /*foreach ($this->_observers as $object) {
                $object->{$method}($this, $data);
            }*/
        }

        foreach ($storage as $object) {
            $object->{$method}($this, $data);
        }
    }

	function notifyBeforeSave() {
		$this->_notify('beforeSave');
	}

	function notifyAfterSave() {
		$this->_notify('afterSave');
	}

	function notifyBeforeDelete() {
		$this->_notify('beforeDelete');
	}

	function notifyAfterDelete() {
		$this->_notify('afterDelete');
	}

	function notifyBeforeLinkObject(Workset_Model_Subject_Interface $subject = null) {
		$this->_notify('beforeLinkObject', $subject);
	}

	function notifyAfterLinkObject(Workset_Model_Subject_Interface $subject = null) {
		$this->_notify('afterLinkObject', $subject);
	}

	function notifyBeforeUnlinkObject(Workset_Model_Subject_Interface $subject = null) {
		$this->_notify('beforeUnlinkObject', $subject);
	}

	function notifyAfterUnlinkObject(Workset_Model_Subject_Interface $subject = null) {
		$this->_notify('afterUnlinkObject', $subject);
	}

	/**
	 * Создает связь между объектами
	 *
	 * @param Workset_Model_Object_Abstract $object
	 * @return bool
	 */
    function linkToObject(Workset_Model_Object_Abstract $object, $data = null) {
        if (!$this->_hasRelations) {
            return false;
        }

		//текущий статус модификации
		$status = $object->isModified();

		//сбросим статус
		if (true === $status) {
			$object->setIsModified(false);
		}

		//вызовем обработчики
		$this->notifyBeforeLinkObject($object);

		//результат обработки
		$result = $object->isModified();

		//установим текущий статус модификации
		if ($result !== $status) {
			$object->setIsModified($status);
		}

		if (!$result) {
			$result = $object->getStorage()->linkToObject($object, $this, $data);
		}

		//вызовем обработчики
		if ($result) {
			$this->notifyAfterLinkObject($object);
		}

		return $result;
    }

	/**
	 * Удаляет связь между объектами
	 *
	 * @param Workset_Model_Object_Abstract $object
	 * @return bool
	 */
    function unlinkObject(Workset_Model_Object_Abstract $object) {
        if (!$this->_hasRelations) {
            return false;
        }

		//текущий статус модификации
		$status = $object->isModified();

		//сбросим статус
		if (true === $status) {
			$object->setIsModified(false);
		}

		//вызовем обработчики
		$this->notifyBeforeUnlinkObject($object);

		//результат обработки
		$result = $object->isModified();

		//установим текущий статус модификации
		if ($result !== $status) {
			$object->setIsModified($status);
		}

		if (!$result) {
        	$result = $object->getStorage()->unlinkObject($object, $this);
		}

		if ($result) {
			$this->notifyAfterUnlinkObject($object);
		}

		return $result;
    }

	/**
	 * Удаляет связанные с объектом файлы, сохраненные на диске.
	 *
	 * @return bool
	 */
    function dropFiles($files) {

        foreach ((array)$files as $file) {
            if (is_array($file)) {
                if (isset($file['realpath'])) {
                    $file = new Workset_File_Info($file['realpath']);
                } elseif (isset($file['shortpath'])) {
                    $file = new Workset_File_Info($file['shortpath'], false);
                } else {
                    continue;
                    //throw new Workset_Model_Exception("Undefined object file path");
                }
            }
            //абсолютный путь к файлу
            elseif (is_string($file)) {
                $file = new Workset_File_Info($file);
            }

            if ($file instanceof Workset_File_Info) {
                if (file_exists($file->getRealPath())) {
                    $success = @unlink($file->getRealPath());
                    if (!$success) {
                        throw new Workset_Model_Exception("Can't delete object file");
                    }
                }
            }
        }

        return true;
    }

	/**
     *
     * Implements ArrayAccess Interface
     * @param  $key
     * @param  $value
     * @return void
     */
    function offsetSet($key, $value) {
        if (array_key_exists($key, $this->_appData)) {
            $this->_appData[$key] = $value;
        } else {
			$this->_saveOldData();
            $this->_data[$key] = $value;
            $this->_update = true;
        }
    }

	/**
	 * Implements ArrayAccess Interface
	 * 
	 * @param  $key
	 * @return Workset_Model_ObjectInterface
	 */
    function offsetGet($key) {
        if (array_key_exists($key, $this->_appData)) {
            return $this->_appData[$key];
        } else {
            return $this->_data[$key];
        }
    }

    /**
     * @param  $key
     * @return boolean
     *
     * Implements ArrayAccess Interface
     */
    function offsetExists($key) {
        return isset($this->_data[$key]) || isset($this->_appData[$key]);
    }

    /**
     * @param  $key
     * @return void
     *
     * Implements ArrayAccess Interface
     */
    function offsetUnset($key) {
        if (array_key_exists($key, $this->_appData)) {
            unset($this->_appData[$key]);
        } else {
            unset($this->_data[$key]);
        }

        $this->_update = true;
    }

	function __sleep() {
		$this->_serialized = true;
	}

    function __clone() {}

    function __call($method, $args) {

		array_unshift($args, $this);

        return call_user_func_array(
            array(
                $this->getStorage($this->_throwNullPointerException),
                $method
            ),
            $args
        );

    }

    protected function _setId($id) {
        if (!empty($id)) {
            $this->_data['id'] = $id;
        }
    }

	/**
	 * Инстанцирует вспомогательный объект
	 *
	 * @param  string $type
	 * @param  string $name
	 * @return mixed
	 */
	static function getPlugin($type, $name) {
		$type = strtolower($type);
		if (isset(self::$_pluginLoaders[$type])) {
			$prefix2Path = self::$_pluginLoaders[$type];
			$key = $type.$name;

			$registry = self::getRegistry();

			if (isset($registry[$key])) {
				return $registry[$key];
			} else {
				$loader = new Zend_Loader_PluginLoader($prefix2Path);
				$class = $loader->load($name);
				$object = new $class();
				self::addToRegistry($key, $object);
				return $object;
			}
		}
	}

}

?>